上一章講解了什麼是指標,當學會了指標的基本操作後,接下來,要說明指標應用在哪。
最簡單的應用在函式的參數傳遞。
函式的參數傳遞就是呼叫函式時,資料如何透過參數傳遞到函式的內部,那資料是怎麼透過參數傳遞?
來看一個一般例子:
package main
import "fmt"
// 呼叫函式,傳入參數 x
func main(){
var x int = 10
add(x)
fmt.Println(x)
}
// 接收參數 x,進行加法
func add(x int){
x = x + 10
fmt.Println("x (add) = ", x)
}
執行結果
x (add) = 20
10
先講結論,main 函式的 x 與 add 函式的 x,兩者並不相關。
首先 main 函式有一個整數變數且資料為 10,當呼叫 add 函式時,順便將 10 一起傳遞到 add 函式中,這個過程要做 Pass by Value。
當 add 函式接收到 10 的參數後,賦予一個 x 變數,將其去做 +10 的加法操作,並將 x 印出,便會是 20,並結束 add 函式。
回到 main 函式,此時的 x 並不受到剛剛 add 函式中的 x 所影響,依舊是原先的 10。
這是沒有透過指標的情況下去傳遞參數,叫做 Pass by Value。
但是你還是想要用這種方式去使 main 函式中的 x 變動,可以這樣做:
package main
import "fmt"
func main(){
x := 10
x = add(x)
fmt.Println("x =", x)
}
func add(x int) int {
x = x + 10
fmt.Println("x (add) = ", x)
return x
}
執行結果:
x (add) = 30
x = 30
雖然不透過指標仍然可以更改 main()
的 x,但是這種方法不是很好,因電腦的記憶體中除了有一塊區域要記得 main()
中的 x,還有一塊區域要記得 add()
中的 x,計算完後還要將 add()
的 num 再 return 給 main()
中的 x。
光看上面的步驟就很繁瑣,所以通常不會這樣寫,會用傳遞指標的方式去實現。
如果傳遞的是指標參數呢?
package main
import "fmt"
func main(){
var x int = 10
var xPtr *int = &x
add(xPtr) // 傳遞指標到 add()
fmt.Println(x) // 20
}
func add(xPtr *int){
*xPtr = *xPtr + 10
fmt.Println(*xPtr) // 20
}
一樣先講結論,因為傳遞的是指標,也就是記憶體位址,所以當 add 函式的操作,會連動影響到原本 main 函式。
這樣的結果很像 Ruby 中的 !
,舉例:
arr.map{|itme|block} -> new_ary # 回傳新的陣列且不影響原本的陣列
arr.map!{|itme|block} -> ary # 回傳一個陣列且會影響原本的陣列
整個程式的步驟如下:
首先 main 函式有一個整數變數且資料為 10,隨後建立一個指標變數(xPtr),並將記憶體位址(&x)存放在其指摽變數中。
接著,呼叫 add 函式,並將 xPtr 傳遞過去,也就是把指標變數的記憶體位址傳遞到 add 函式中的參數,這個過程就要 Pass by Pointer。
當記憶體位址傳遞到 add 函式後,因為要對反解過後的資料做運算,所以這邊先將記憶體位址反解過後再 +10,add 函式印出來的資料會是 20。
那反解過後的資料是什麼,也就是 main 函式中的 x,因為是複製一份記憶體位址到 add 函式中,當 add 函式的記憶體位置有變動時,兩者會有連動關係;所以當回到 main 函式中後,印出來的資料也會是 20。
package main
import "fmt"
func add(xPtr *int) {
*xPtr = *xPtr + 10
fmt.Println(*xPtr)
}
func main() {
var a int = 10
add(&a) //不一定要給指標變數
fmt.Println("Main Function", a)
}
會反應到原本的 main 函式。
另外一個之前有用到這個特性的功能。
和使用者要求輸入資料,運用到指標參數 Pass by Pointer:
package main
import "fmt"
func main(){
var msg string
// 傳遞字串變數的指標(記憶體位址)
fmt.Scanln(&msg)
fmt.Println(msg)
}
Scanln 內部的程式就會傳遞指標,並反解以及改變 msg 的資料,去反應在程式中,才能順利的拿到使用者輸入的資料。
當然也可以寫成這樣:
package main
import "fmt"
func main(){
var msg string
var msgPtr *string = &msg
fmt.Scanln(msgPtr)
fmt.Println(msg)
}
Slice 本質也是類似一個指標,所以傳遞切片時可以直接 Slice 中的值。
package main
import "fmt"
func main(){
s1 := []int{1, 2, 3, 4, 5}
s2 := s1
s2[0] = 120
fmt.Println(s1)
fmt.Println(s2)
}
執行結果
[120 2 3 4 5]
[120 2 3 4 5]
另一個例子:
package main
import "fmt"
func double(nums []int){
for i := 0; i < len(nums); i++{
nums[i] = nums[i] * 2
}
}
func main(){
nums := []int{1, 3, 4, 7, 9}
double(nums)
for _, v := range nums{
fmt.Printf("%d", v)
}
fmt.Printf("%d", nums)
}
執行結果:
2 6 8 14 18
可以看到在將切片指派給新的變數時,實際上是將原始切片的指標複製一份給新的變數,所以在修改新的變數時,原始切片也會跟著被修改。
Map 是一種引用類型,當它作為函式參數傳遞時,會傳遞指向 Map 的指標。
package main
import "fmt"
func plusOne(m map[string]int){
for k, v := range m{
m[k] = v + 100
}
}
func main(){
price := map[string]int{
"apple": 20,
"kiwi": 38,
"melon": 69,
"orange": 43,
}
plusOne(price)
for k, v := range price{
fmt.Printf("%s: %d\n", k, v)
}
}
執行結果
apple: 120
kiwi: 138
melon: 169
orange: 143
在 Go 語言中,陣列是一種值類型,而不是指標類型,這意味著當你將一個陣列賦值給另一個變數時,會複製一份全新的陣列,而不是將指標指向同一個陣列。
package main
import "fmt"
func main(){
nums := [5]int{1, 3, 4, 7, 9}
double(nums)
for _, v := range nums{
fmt.Printf("%d", v)
}
}
func double(nums [5]int){
for i := 0; i < len(nums); i++{
nums[i] = nums[i] * 2
}
}
執行結果
1 3 4 7 9
雖然在函式 double()
中更改了 nums
,但因為陣列 nums
傳進函式 double()
是傳送「值」而不是「址」所以並不會影響原先的陣列。
func main() {
arr1 := [3]int{1, 2, 3}
arr2 := arr1
arr2[0] = 4
fmt.Println(arr1) // [1 2 3]
fmt.Println(arr2) // [4 2 3]
}
執行結果
[1 2 3]
[4 2 3]
宣告了一個陣列 arr1
,並將其複製到了 arr2
中。當 arr2
的第一個元素改為 4
時,arr1
並沒有受到影響,因為它們是兩個獨立的陣列。這兩個例子證明了 Go 中的陣列不是指標。
如果你想透過函式更改陣列的值,可以透過取址的方式將其實現。
package main
import "fmt"
func double(nums *[5]int){
for i := 0; i < len(nums); i = i+1{
(*nums)[i] = (*nums)[i] * 2
}
}
func main(){
nums := [5]int{1, 3, 4, 7, 9}
double(&nums)
for _, v := range nums{
fmt.Printf("%d ", v)
}
}
執行結果
2 6 8 14 18
參考資料: